
package cz.mpelant.msoe.mealchecker.downloader;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.EofSensorInputStream;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

public class Downloader implements Runnable {

    private static final String TAG = "Downloader";
    private static int MAX_DOWNLOAD_THREADS = 1;
    private static final long DEFAULT_MIN_TIME_BETWEEN_SAME_REQUESTS = 1;
    private static final String PREF_FILE = "cz.eman.android.lib.downloader.history";

    public static final String EXTRA_STATUS = "status";
    public static final int STATUS_STARTED = 0;
    public static final int STATUS_FINISHED = 1;

    public static int TIMEOUT_CONNECTION = 10000;
    public static int TIMEOUT_SOCKET = 60000;

    private static Downloader instance;
    private Context mAppCtx;
    private SharedPreferences sharedPref;
    private List<DownloadItem> queue;
    // whether the thread that redistributes request to DownloadTasks is running
    private boolean redistributing = false;
    private List<DownloadTask> downloadTasks;
    private Handler mHandler = new Handler(Looper.getMainLooper());
    private DefaultHttpClient mHttpClient;
    private static long minTimeBetweenReq;

    /**
     * Download item passed to DownloadTask
     */
    private class DownloadItem {
        private DownloadRequest request;
        private DownloadListener listener;

        public DownloadItem(DownloadRequest request, DownloadListener listener) {
            this.request = request;
            this.listener = listener;
        }

        public DownloadListener getListener() {
            return listener;
        }

        public DownloadRequest getRequest() {
            return request;
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof DownloadItem)
                return ((DownloadItem) o).request.equals(request);
            return false;
        }

    }

    private class DownloadTask extends Thread {

        private boolean cancelled = false;
        // Background thread which doInBackground runs in
        private Thread thread;
        private DownloadItem item;

        @Override
        public boolean equals(Object o) {
            if (o instanceof DownloadTask)
                return ((DownloadTask) o).item.equals(item);
            return false;
        }

        public DownloadTask(DownloadItem item) {
            this.item = item;
        }

        @Override
        public void run() {
            preExecute();
            postExecute(doInBackground());
            super.run();
        }

        public DownloadTask execute() {
            this.start();

            return this;
        }

        public void cancel() {
            cancelled = true;
            thread.interrupt();
        }

        /**
         * executes onProgressUpdate() in the main thread
         *
         * @param progress
         * @param httpResponse
         */
        private void publishProgress(final int progress, final HttpResponse httpResponse) {
            mHandler.post(new Runnable() {

                @Override
                public void run() {
                    onProgressUpdate(progress, httpResponse);
                }
            });
        }

        /**
         * executes preExecute() in the main thread
         */
        private void preExecute() {
            mHandler.post(new Runnable() {

                @Override
                public void run() {
                    onPreExecute();
                }
            });
        }

        /**
         * executes postExecute() in the main thread
         *
         * @param result
         */
        private void postExecute(final DownloadResult result) {
            mHandler.post(new Runnable() {

                @Override
                public void run() {
                    onPostExecute(result);
                }
            });
        }

        protected void onPreExecute() {
            item.getListener().onDownloadStarted(item.getRequest(), mAppCtx);
        }

        protected DownloadResult doInBackground() {
            thread = Thread.currentThread();
            DownloadRequest request = item.getRequest();

            HttpResponse response = null;

            InputStream input = null;
            OutputStream output = request.getOs();
            Log.d(TAG, request.getHttpRequest().getURI().toString());

            for (Header header : request.getHttpRequest().getAllHeaders()) {
                Log.d(TAG, header.toString());
            }

            // try to get headers request.getRetries() many times
            for (int i = 0; i < request.getRetries() && !cancelled; i++) {
                try {
                    response = mHttpClient.execute(request.getHttpRequest());
                    int statusCode = response.getStatusLine().getStatusCode();
                    Log.d(TAG, "status code: " + statusCode);
                    if (statusCode == HttpStatus.SC_OK) {
                        input = response.getEntity().getContent();
                    }
                    break;
                } catch (Exception e) {
                    Log.d(TAG, "retry " + i);
                    if (i + 1 == request.getRetries())
                        return new DownloadResult(request, DownloadResult.DOWNLOAD_ERROR, response);
                    continue;
                }
            }

            if (response.getEntity() != null) {
                long lenghtOfFile = response.getEntity().getContentLength();
                long total = 0;
                int count;
                byte buffer[] = new byte[1024];

                // try downloading body
                try {
                    while (!cancelled && (count = input.read(buffer)) != -1) {
                        total += count;

                        publishProgress((int) (total * 100 / lenghtOfFile), response);
                        output.write(buffer, 0, count);
                    }
                    if (cancelled) {
                        return new DownloadResult(request, DownloadResult.DOWNLOAD_CANCELLED, response);
                    }
                } catch (Exception e) {
                    return new DownloadResult(request, DownloadResult.DOWNLOAD_ERROR, response);
                } finally {
                    try {
                        if (!cancelled)
                            output.flush();
                    } catch (Exception e) {
                    }
                    try {
                        output.close();
                    } catch (Exception e) {
                    }
                    try {
                        // special version of close() method that doesn't
                        // download
                        // the rest of the content
                        ((EofSensorInputStream) input).abortConnection();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            publishProgress(100, response);

            return new DownloadResult(request, DownloadResult.DOWNLOAD_FINISHED, response);
        }

        protected void onProgressUpdate(int progress, HttpResponse httpResponse) {
            item.getListener().onProgressUpdate(new DownloadResult(item.getRequest(), progress, httpResponse), mAppCtx);
        }

        protected void onPostExecute(DownloadResult result) {
            item.getListener().onDownloadFinished(result, mAppCtx);
            removeTaskFromList(this);
            addToHistory(item);
        }
    }

    private Downloader(Context ctx) {
        this.mAppCtx = ctx.getApplicationContext();
        queue = new ArrayList<Downloader.DownloadItem>();
        downloadTasks = new ArrayList<Downloader.DownloadTask>();
        sharedPref = mAppCtx.getSharedPreferences(PREF_FILE, 0);
        minTimeBetweenReq = DEFAULT_MIN_TIME_BETWEEN_SAME_REQUESTS;
        mHttpClient = createAllTrustHttpClient();
    }

    public static Downloader getInstance(Context ctx) {
        if (instance == null) {
            instance = new Downloader(ctx);
        }
        return instance;
    }

    private synchronized void notifyDataAdded() {
        if (!isRedistributing()) {
            setRedistributing(true);
            new Thread(this).start();
        }
        notifyAll();
    }

    private synchronized void push(DownloadItem item) {
        queue.add(item);
    }

    private synchronized DownloadItem pop() {
        if (queue.size() > 0)
            return queue.remove(0);
        setRedistributing(false);
        return null;
    }

    public synchronized void cancelDownloads() {

        for (DownloadTask task : downloadTasks) {
            task.cancel();
        }
        queue.clear();
        setRedistributing(false);
        notifyAll();
    }

    /**
     * @return true if Downloader is downloading something, false otherwise
     */
    public boolean isRunning() {
        if (isRedistributing() || downloadTasks.size() > 0)
            return true;
        return false;
    }

    public boolean downloadForced(DownloadRequest request, DownloadListener listener) {
        return downloadForced(request, listener, true);
    }

    public boolean download(DownloadRequest request, DownloadListener listener) {
        return download(request, listener, true);
    }

    public synchronized boolean downloadForced(DownloadRequest request, DownloadListener listener, boolean checkDuplicateRequests) {
        DownloadItem downloadItem = new DownloadItem(request, listener);
        if (checkDuplicateRequests && isRequestDownloading(request))
            return false;
        push(downloadItem);
        notifyDataAdded();
        return true;

    }

    /**
     * returns if the download was added (false if checkDuplicateRequests is
     * true and a duplicate entry is already in the queue)
     *
     * @param request
     * @param listener
     * @param checkDuplicateRequests
     * @return
     */
    public synchronized boolean download(DownloadRequest request, DownloadListener listener, boolean checkDuplicateRequests) {
        if (!canBeDownloaded(request))
            return false;
        return downloadForced(request, listener, checkDuplicateRequests);
    }

    public boolean isRequestDownloading(DownloadRequest request) {
        DownloadItem downloadItem = new DownloadItem(request, null);
        if (queue.contains(downloadItem) || downloadTasks.contains(new DownloadTask(downloadItem)))
            return true;
        return false;
    }

    /**
     * Blocks thread until there is less working threads than
     * MAX_DOWNLOAD_THREADS
     *
     * @return
     */
    private boolean waitForAvailableThread() {
        while (downloadTasks.size() >= MAX_DOWNLOAD_THREADS) {
            try {
                synchronized (this) {
                    wait(10000);
                }
            } catch (InterruptedException e) {
            }
        }
        return true;
    }

    private synchronized void removeTaskFromList(DownloadTask task) {
        downloadTasks.remove(task);
        notifyAll();
        if (downloadTasks.isEmpty())
            notifyDownloadsFinished();
    }

    public String getActionDownloadStatus() {
        return mAppCtx.getPackageName() + ".downloader.STATUS_CHANGED";
    }

    private void notifyDownloadsFinished() {
        mAppCtx.sendBroadcast(new Intent(getActionDownloadStatus()).putExtra(EXTRA_STATUS, STATUS_FINISHED));
        Log.d(TAG, getActionDownloadStatus() + " :" + STATUS_FINISHED);

    }

    private void notifyDownloadsStarted() {
        mAppCtx.sendBroadcast(new Intent(getActionDownloadStatus()).putExtra(EXTRA_STATUS, STATUS_STARTED));
        Log.d(TAG, getActionDownloadStatus() + " :" + STATUS_STARTED);
    }

    private synchronized void addTaskToList(DownloadTask task) {
        downloadTasks.add(task);
    }

    /**
     * redistributes requests by creating new DownloadTasks
     *
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        notifyDownloadsStarted();
        DownloadItem item = null;
        while (waitForAvailableThread() && (item = pop()) != null) {
            addTaskToList(new DownloadTask(item).execute());
        }
        setRedistributing(false);
    }

    private boolean isRedistributing() {
        return redistributing;
    }

    private void setRedistributing(boolean redistributing) {
        Intent serviceIntent = new Intent(mAppCtx, DummyService.class);
        if (redistributing == true) {
            mAppCtx.startService(serviceIntent);
        } else {
            mAppCtx.stopService(serviceIntent);
        }
        this.redistributing = redistributing;
    }

    public static void setMinTimeBetweenSameRequests(long time) {
        minTimeBetweenReq = time;
    }

    public static void setMaxThreads(int max_threads) {
        MAX_DOWNLOAD_THREADS = max_threads;
    }

    private void addToHistory(DownloadItem item) {
        sharedPref.edit().putLong(item.getRequest().getId(), System.currentTimeMillis()).commit();
    }

    private boolean canBeDownloaded(DownloadRequest request) {
        return (System.currentTimeMillis() - sharedPref.getLong(request.getId(), 0) > minTimeBetweenReq);
    }

    /**
     * clears hsitory of Requests (used for MIN_TIME_BETWEEN_SAME_REQUESTS
     */
    public void clearRequesHistory() {
        sharedPref.edit().clear().commit();
    }

    
    public void clearCookies(){
    	new DefaultHttpClient().getCookieStore().clear();
    }
    private DefaultHttpClient createAllTrustHttpClient() {
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(null, null);

            SSLSocketFactory sf = new AndroidSSLSocketFactory(trustStore);
            sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

            HttpParams params = new BasicHttpParams();
            HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);
            HttpConnectionParams.setConnectionTimeout(params, TIMEOUT_CONNECTION);
            HttpConnectionParams.setSoTimeout(params, TIMEOUT_SOCKET);

            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register(new Scheme("https", sf, 443));

            ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

            return new DefaultHttpClient(ccm, params);
        } catch (Exception e) {
            return new DefaultHttpClient();
        }
    }

}
